package com.citrix.gs.util.beacon;

import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.lang.reflect.Field;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.io.SAXReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.citrix.gs.kit.EmailSender;
import com.citrix.gs.util.beacon.job.BeaconJob;
import com.citrix.gs.util.common.CIInfo;
import com.citrix.gs.util.common.Config;
import com.citrix.gs.util.common.SrcInfo;
import com.offbytwo.jenkins.JenkinsServer;
import com.offbytwo.jenkins.client.JenkinsHttpClient;
import com.offbytwo.jenkins.model.Build;
import com.offbytwo.jenkins.model.Job;
import com.offbytwo.jenkins.model.JobWithDetails;

public class BeaconCI extends JenkinsServer {
    
    private static Logger logger = LoggerFactory.getLogger(BeaconCI.class);
            
    private URI uri;
    protected JenkinsHttpClient client;
    
    @Override
    public String toString() {
        return "BeaconCI: " + uri.toString();
    }

    public BeaconCI(URI serverUri) {
        super(serverUri);
        
        this.uri = serverUri;
        try {
            Field f = this.getClass().getSuperclass().getDeclaredField("client");
            f.setAccessible(true);
            client = (JenkinsHttpClient) f.get((JenkinsServer)this);
            
        } catch (Exception e) {
            logger.error("Reflect to get jenkins client fail: ", e);
        } 
    }

    public BeaconCI(URI serverUri, String username, String passwordOrToken) {
        super(serverUri, username, passwordOrToken);
        
        this.uri = serverUri;
        try {
            Field f = this.getClass().getSuperclass().getDeclaredField("client");
            f.setAccessible(true);
            client = (JenkinsHttpClient) f.get((JenkinsServer)this);
            
        } catch (Exception e) {
            logger.error("Reflect to get jenkins client fail: ", e);
        } 
    }
    
    public JenkinsHttpClient getClient() {
        return client;
    }

    public Document getJobDocument(String jobName) {
        
        Document doc = null;
        try {
            String xml = getJobXml(jobName);
            SAXReader reader = new SAXReader();
            doc = reader.read(new StringReader(xml));
            
        } catch (IOException e) {
            logger.info("Fail to get xml of job {}", jobName, e);
        } catch (DocumentException e) {
            logger.info("Fail to parse xml of job {}", jobName, e);
        }
        
        return doc;
    }
    
    public URI getUri() {
        return uri;
    }
    
    public String getHostURL(){
        return String.format("%s://%s:%d", uri.getScheme(), uri.getHost(), uri.getPort());
    }

    public BeaconCI(CIInfo ciInfo) {
        this(ciInfo.getURI(), ciInfo.getUsername(), ciInfo.getPassword());
    }
    
    public int getQueueSize() {
        StringBuilder sb = new StringBuilder();
        sb.append("import hudson.model.*"); sb.append("\n");
        sb.append("def queue = Hudson.instance.queue");  sb.append("\n");
        sb.append("print queue.items.length");  sb.append("\n");
        
        int size = -1;
         
        try {
            String result = this.runScript(sb.toString());
            size = Integer.valueOf(result);
            
         } catch (IOException e) {
             e.printStackTrace();
         }
        
        return size;
    }
    
    public int getBusyExecutors() {
        StringBuilder sb = new StringBuilder();
        sb.append("import jenkins.model.*"); sb.append("\n");
        sb.append("Jenkins.instance.computers.each() { c -> print(c.countBusy())}"); sb.append("\n");
        sb.append("print(0)"); sb.append("\n");
        
        //sb.append("print(Jenkins.instance.computers.inject(0, {a, c -> a + c.countBusy()}))");  sb.append("\n");
        
        int count = -1;
        try {
            String result = this.runScript(sb.toString());
            logger.debug("The result of run script in getBusyExecutors: {}", result);
            
            //count = Integer.valueOf(result);
            int sum = 0;
            for(int i=0; i<result.length(); i++){
                sum += Integer.valueOf("" + result.charAt(i));
            }
            count = sum;
        } catch (NumberFormatException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        
        return count;
    }
    
    public int getIdleExecutors(char group) {
        
        int count = 0;
        
        StringBuilder sb = new StringBuilder();
        sb.append("import jenkins.model.*"); sb.append("\n");
        sb.append("Jenkins.instance.computers.each() { c -> print \"${c.displayName}:${c.countIdle()};\" };"); sb.append("\n");
        sb.append("println()"); sb.append("\n");
        
        try {
            String result = this.runScript(sb.toString());
            logger.debug("Result: {}", result);
            
            String[] machines = result.split(";");
            
            for(String machine : machines) {
                logger.debug("Machine: {}", machine);
                if(!(machine.trim().isEmpty()) && machine.startsWith("Slave")) {
                    String[] tmp = machine.trim().split(":");
                    logger.debug("Machine: {}, Count: {}", tmp[0], tmp[1]);
                    if(tmp[0].charAt(5) == group) {
                        count += Integer.valueOf(tmp[1]);
                    }
                }
            }
            
        } catch (IOException e) {
            e.printStackTrace();
        }
        
        return count;
       
    }
    
    public SrcInfo getLastSrcInfo(String repo){
        
        Properties prop = getLastSrcInfoProp(repo);
        
        if(prop == null) return null;
        
        return new SrcInfo(prop);
    }
    
    public Properties getLastSrcInfoProp(String repo){
        Properties prop = new Properties();
        
        String lastSrcInfoUrl = String.format("%s/job/Src-%s/lastSuccessfulBuild/artifact/SrcInfo.properties", getHostURL(), repo);
        
        try {
            InputStream is = client.getFile(new URI(lastSrcInfoUrl));
            prop.load(is);
            
        } catch (IOException | URISyntaxException e) {
 
            logger.error("Fail to get {}", lastSrcInfoUrl, e);
            return null;
        }
        
        
        return prop;
    }
    
    private List<String> getJobNames(String repo, Map<String, Job> jobs)  {
        Pattern pattern = Pattern.compile(".*-" + repo.toLowerCase());
        return jobs.keySet().stream().filter(n -> pattern.matcher(n).matches())
                .map(key -> {
                    return jobs.get(key).getName();
                })
                .collect(Collectors.toList());
    }
    
    // Get name of jobs whose name include repo names
    public List<String> getJobNames(String repo)  {
        
        Map<String, Job> jobs;
              
        try {
            jobs = super.getJobs();   
        } catch (IOException e) {
            logger.error("Get jobs fail: ", e);
            return null;
        }
        
        return getJobNames(repo, jobs);
    }

    
    public List<String> getJobNames(List<String> repos) {
        
        Map<String, Job> jobs;
        
        try {
            jobs = super.getJobs();   
        } catch (IOException e) {
            logger.error("Get jobs fail: ", e);
            return null;
        }
        
       return repos.stream().flatMap(repo -> getJobNames(repo, jobs).stream()).collect(Collectors.toList());
    }
    
    
    public List<Job> getViewJobs(String viewName, Date beforeDate) {
        try {
            return getView(viewName).getJobs().stream().filter(job -> {
                try {
                    job.setClient(client); // Workaround for a bug in jenkins client
                    JobWithDetails jd = job.details(); 
                    Build build = jd.getLastSuccessfulBuild();
                    return build == null || build.details().getTimestamp() < beforeDate.getTime();
                } catch (Exception e) {
                    e.printStackTrace();
                    return true;
                }
            }).collect(Collectors.toList());
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        } 
    }
    
    private void addJobToList(Job job, List<Job> jobs) {
        
        boolean bExist = false;
        for(Job existJob : jobs){
            if(existJob.getName().equals(job.getName())){
                bExist = true;
                break;
            }
        }
        if(!bExist){
            jobs.add(job);
        }
    }
    
    
    public boolean addJob(String jobName, String confXml, boolean force) {
        
        try {
            if(super.getJob(jobName) != null ) {
                
                if(force){
                    super.deleteJob(jobName, false);
                } else {
                    logger.error("Job {} has been exist in CI: {}", jobName, uri);
                    return false;
                }
                
            }
            
            super.createJob(jobName, confXml ,false);
        } catch (IOException e) {
            logger.error("Fail to create job {}", jobName, e);
        }
        
        return true;
    }

    // TODO: Optimize it with function mode
    public List<Job> getTopJobs(List<Job> jobs){
        
        List<Job> topJobs = new ArrayList<Job>();
        int round = 0;
        
        do {
            logger.info(String.format("Get top jobs: Round %d, job size %d", ++round, jobs.size()));
            List<Job> upperJobs = new ArrayList<Job>();
            
            for(Job job : jobs) {
                
                JobWithDetails details = null;
                try {
                    if(job.getClient() == null){
                        details = getJob(job.getName()).details();
                    } else {
                        details = job.details();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
                
                List<Job> ups = details.getUpstreamProjects();
                if(ups.size() == 0) {
                    addJobToList(job, topJobs);
                } else {
                    // TODO: improve the filter exist strategy
                    for(Job upJob : ups) {
                       addJobToList(upJob, upperJobs);
                    }
                }
            }
            
            //jobs.clear();
            jobs = upperJobs;
                    
        } while(jobs.size() > 0);
        
        return topJobs;
    }
    
    public void runTopJobs(List<Job> topJobs) {
        runJobs(topJobs);
    }
    
    /** Run jobs in a friendly way to prevent too may builds keep up in waiting queue
     * @param jobs : jobs to be started, all elements will be empty at last
     */
    public void runJobs(List<Job> jobs) {
        
        Date startDate = new Date();
        
        int size = jobs.size();
        
        while (size > 0) {
            
            for(int i=size-1; i>-1; i--) {
                try {
                    // check if queue size too much:
                    int queueSize =0;
                    while((queueSize = this.getQueueSize()) > 10) {
                        logger.info("There are {} jobs in wait queue, hold on one minute...", queueSize);
                        TimeUnit.MINUTES.sleep(1);
                    }
                    
                    Job job = jobs.get(i);
                    String jobName = job.getName();
                    Document doc = this.getJobDocument(jobName);
                    BeaconJob beaconJob = new BeaconJob().setDoc(doc);
                    
                    if(beaconJob.isDisabled()) { // disabled job, remove it directly
                        jobs.remove(i);
                        continue;
                    }
                    
                    String node = beaconJob.getAssignedNode();
                    
                    char group = node.charAt(node.length()-1);
                    
                    if(this.getIdleExecutors(group) > 0) {
                        
                        logger.info("Build job: {} ", jobName);
                        if(job.getClient() == null) { // TODO: workaround for jenkins client is null when get job
                            job.setClient(this.getClient());
                        }
                        job.build(false);
                        jobs.remove(i);
                        logger.info("{} jobs left", jobs.size());
                        //if(jobs.size() > 0) {
                            logger.info("Wait 1 minute before trigger next job or check run over ...");
                            TimeUnit.MINUTES.sleep(1);
                        //}
                        
                    } else {
                        TimeUnit.SECONDS.sleep(5);
                    }
                
                } catch(Exception e) {
                    logger.error("Failed to build job: {}", jobs.get(i).getName(), e);
                }
            }
            
            size = jobs.size();
        }
        
        logger.info("All top jobs are triggered");
        
        // Wait for all jobs run over
        while(this.getBusyExecutors() > 0 || this.getQueueSize() > 0) {
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                // ignore the exception
            }
        }
        
        logger.info("No busy Executors now, all are run over");
        
        Date endDate = new Date();
        
        try {
            EmailSender.getInstance().send("Run jobs over!!!", 
                    new String[] { Config.me().getProperty("admin.email") }, 
                    String.format("Run jobs on %s \r\n Start: %s \r\n End: %s", this, startDate, endDate));
        } catch (Exception e) {
            logger.error("Fail to send notification email", e);
        }
    }
    
    public static void main(String [] args) throws URISyntaxException, IOException{
        
        //URI uri = new URI("http://beacon-test-ci.eng.citrite.net:8080");
        //System.out.println(uri.getScheme() + uri.getHost() + uri.getPort());
        
        
        BeaconCI myci = new BeaconCI(new URI("http://beacon-ci.eng.citrite.net:8080"), "yuzho", "Sqzy$1315");
        
        myci.getJobNames("XenMobile-WorxHomeAndroid-Gauntlet1").forEach(System.out::println);
        
    }

    public void runRepositories(List<String> repoList) {
        
        final BeaconCI ci = this;
        
        runJobs(repoList.stream().map(r -> {
            try {
                return ci.getJob(BeaconJobType.Src.name() + "-" + r);
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }).filter(job -> job != null)
        .collect(Collectors.toList()));
        
    }

}
